package Halstead;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Clase para el cálculo de Halstead
 */
public class Halstead {

	/**
	 * Expresión regular para encontrar el identidicador de una variable
	 * 
	 * Por convención, el identificador de una variable debe empieza con una
	 * letra minúscula, mayúscula, con "_" o el signo "$". Luego de ésta letra
	 * inicial se puede combinar agregando números tambien.
	 */
	static private String IDENTIFICADOR_REGEX = "^[A-Za-z\\_\\$][A-Za-z0-9\\_\\$]*$";

	/**
	 * Expresión regular para encontrar un número
	 */
	static private String CONSTANTE_NUMERICA_REGEX = "[0-9]+";

	/**
	 * Expresión regular para encontrar una palabra reservada para operadores
	 */
	static private String OPERADOR_REGEX = "(auto|extern|inlin|register|static|typedef|virtual|mtuable|asm|break|case|class|continue|default|delete|do|else|enum|for|goto|if|new|operator|private|protected|public|return|sizeof|struct|switch|this|union|while|namespace|using|try|catch|throw|const_cast|static_cast|dynamic_cast|reinterpret_cast|typeid|template|explicit|true|false|typename)";

	/**
	 * Expresión regular para encontrar una función
	 */
	static private String FUNCION_NAME_REGEX = "[A-Za-z]+[A-Za-z\\.]*(\\()";

	/**
	 * Número de operadores únicos que aparecen en un programa.
	 */
	private Integer n1;
	/**
	 * Número total de ocurrencias de operadores.
	 */
	private Integer N1;
	/**
	 * Número de operandos únicos que aparecen en un programa.
	 */
	private Integer n2;
	/**
	 * Número total de ocurrencias de operandos.
	 */
	private Integer N2;

	// GETTERS
	public Integer getn1() {
		return n1;
	}

	public Integer getN1() {
		return N1;
	}

	public Integer getn2() {
		return n2;
	}

	public Integer getN2() {
		return N2;
	}

	/**
	 * Lista con los operadores y su cantidad de repeticiones en el código
	 */
	private HashMap<String, Integer> listaOperadores;

	/**
	 * Lista con los operandos y la cantidad de repeticiones en el código
	 */
	private HashMap<String, Integer> listaOperandos;

	/**
	 * Constructor de la clase Halstead
	 */
	public Halstead() {

		n1 = new Integer(0);
		N1 = new Integer(0);
		n2 = new Integer(0);
		N1 = new Integer(0);

		// Se inicializa el Hashmap de operadores
		listaOperadores = new HashMap<String, Integer>();
		listaOperadores.put("for(;;)", 0);
		listaOperadores.put("if", 0);
		listaOperadores.put("while", 0);
		listaOperadores.put("++", 0);
		listaOperadores.put("--", 0);
		listaOperadores.put("+", 0);
		listaOperadores.put("-", 0);
		listaOperadores.put("/", 0);
		listaOperadores.put("*", 0);
		listaOperadores.put(">", 0);
		listaOperadores.put("<", 0);
		listaOperadores.put(">=", 0);
		listaOperadores.put("<=", 0);
		listaOperadores.put("!", 0);
		listaOperadores.put("!=", 0);
		listaOperadores.put("==", 0);
		listaOperadores.put("=", 0);
		listaOperadores.put("||", 0);
		listaOperadores.put("&&", 0);

		// Se inicializa el HashMap de operandos
		listaOperandos = new HashMap<String, Integer>();

	}

	/**
	 * Calcula la cantidad de ocurrencias de un String dentro de una linea
	 */
	private int cantidad(String linea, String c) {

		String[] splitteado = linea.split(c);

		return splitteado.length - 1;
	}

	/**
	 * Parsea una linea para obtener los operadores
	 * 
	 * @param linea
	 *            - linea a parsear
	 */
	private void parsearOperadores(String linea) {

		boolean sinCondicional = true;

		// Se buscan condicionales
		if (linea.contains("for(") || linea.contains("for ")) {
			ponerOperador("for(;;)", 1);
			sinCondicional = false;

		} else if (linea.contains("if(") || linea.contains("if ")) {
			ponerOperador("if", 1);
			ponerOperador("(..)", 1);
			sinCondicional = false;

		} else if (linea.contains("while(") || linea.contains("while ")) {
			ponerOperador("while", 1);
			ponerOperador("(..)", 1);
			sinCondicional = false;

		}

		// Se buscan operadores aritméticos
		if (linea.contains("++")) {
			ponerOperador("++", cantidad(linea, "\\+\\+"));

		} else if (linea.contains("--")) {
			ponerOperador("--", cantidad(linea, "\\-\\-"));

		} else if (linea.contains("+")) {
			ponerOperador("+", cantidad(linea, "\\+"));

		} else if (linea.contains("-")) {
			ponerOperador("-", cantidad(linea, "\\-"));

		} else if (linea.contains("*")) {
			ponerOperador("*", cantidad(linea, "\\*"));

		} else if (linea.contains("/")) {
			ponerOperador("/", cantidad(linea, "\\/"));

		}

		// Se buscan operadores lógicos
		if (linea.contains("<=")) {
			ponerOperador("<=", cantidad(linea, "<="));

		}
		if (linea.contains(">=")) {
			ponerOperador(">=", cantidad(linea, ">="));

		}
		if (linea.contains("==")) {
			ponerOperador("==", cantidad(linea, "=="));

		}
		if (linea.contains("!=")) {
			ponerOperador("!=", cantidad(linea, "!="));

		}
		if (linea.contains(">")) {
			ponerOperador(">", cantidad(linea, ">"));

		}
		if (linea.contains("<")) {
			ponerOperador("<", cantidad(linea, "<"));

		}
		if (linea.contains("&&")) {
			ponerOperador("&&", cantidad(linea, "&&"));

		}
		if (linea.contains("||")) {
			ponerOperador("||", cantidad(linea, "||"));

		}
		if (linea.contains("!")) {
			ponerOperador("!", cantidad(linea, "!"));

		}
		if (linea.contains("=")) {
			ponerOperador("=", cantidad(linea, "="));

		}

		if (sinCondicional == true) {
			if (linea.contains(";")) {
				ponerOperador(";", 1);
			}

			if (linea.contains("{")) {
				ponerOperador("{..}", 1);

			}
			if (linea.lastIndexOf('[') != -1) {
				ponerOperador("[]", cantidad(linea, "\\["));

			}
			if (linea.lastIndexOf('(') != -1) {
				ponerOperador("(..)", cantidad(linea, "\\("));

			}
		}

		// Se eliminan los espacios en blanco
		linea = linea.replace(" ", "");
		linea = linea.replace("\t", "");
		// Se cambian por un espacio los caracteres especiales
		linea = linea.replace(";", " ");
		linea = linea.replace(")", " ");
		linea = linea.replace("[", " ");
		linea = linea.replace("]", " ");
		linea = linea.replace("++", " ");
		linea = linea.replace("--", " ");
		linea = linea.replace("=", " ");
		linea = linea.replace("!", " ");
		linea = linea.replace("<", " ");
		linea = linea.replace(">", " ");
		String splitteado[] = linea.split(" ");
		for (String auxString : splitteado) {

			if (auxString.matches(FUNCION_NAME_REGEX)) {

				auxString.replace("(", "");
				ponerOperador(auxString, 1);
			}
		}

	}

	/**
	 * Función que parsea dentro de una linea los operandos
	 * 
	 * @param linea
	 *            - linea a parsear
	 */
	private void parsearOperandos(String linea) {

		// Se eliminan los espacios en blanco
		linea = linea.replace(" ", "");
		linea = linea.replace("\t", "");

		// Se cambian por un espacio los caracteres especiales
		linea = linea.replace("(", " ");
		linea = linea.replace(";", " ");
		linea = linea.replace(")", " ");
		linea = linea.replace("[", " ");
		linea = linea.replace("]", " ");
		linea = linea.replace("++", " ");
		linea = linea.replace("--", " ");
		linea = linea.replace("=", " = ");
		linea = linea.replace("!", " ! ");
		linea = linea.replace("<", " < ");
		linea = linea.replace(">", " > ");

		// Se separa la linea
		String[] splitteado = linea.split(" ");
		for (String aux : splitteado) {

			// Si no es una palabra reservada
			if (!aux.matches(OPERADOR_REGEX)
					&& !aux.matches(FUNCION_NAME_REGEX)) {

				// Si coincide con un identificador se agrega
				if (aux.matches(IDENTIFICADOR_REGEX)) {

					ponerOperando(aux, 1);

					// Sino, si coincide con un valor numerico se agrega
				} else if (aux.matches(CONSTANTE_NUMERICA_REGEX)) {

					ponerOperando(aux, 1);
				}
			}
		}
	}

	/**
	 * Función que agrega un operador, en caso de existir suma en uno la
	 * cantidad total
	 */
	private void ponerOperador(String operador, Integer i) {

		if (listaOperadores.containsKey(operador) == false) {

			listaOperadores.put(operador, 1);
		} else {

			listaOperadores.put(operador, listaOperadores.get(operador) + i);
		}
	}

	/**
	 * Función que agrega un operando, en caso de existir suma en uno la
	 * cantidad total
	 */
	private void ponerOperando(String operando, Integer i) {

		if (listaOperandos.containsKey(operando) == false) {

			listaOperandos.put(operando, 1);
		} else {

			listaOperandos.put(operando, listaOperandos.get(operando) + i);
		}
	}

	/**
	 * Calcula la longitud Halstead de la función
	 */
	public Integer longitud() {

		return N1 + N2;
	}

	/**
	 * Calcula el volumen Halstead de la función
	 */
	public Double volumen() {

		// Se calcula el volumen y se redondea a dos decimales
		double valor = longitud() * (Math.log(n1 + n2) / Math.log(2));
		BigDecimal bd = new BigDecimal(valor);
		bd = bd.setScale(2, RoundingMode.HALF_UP);

		return bd.doubleValue();
	}

	/**
	 * Aplica el método Halstead a una función
	 */
	public void aplicarMetodo(String textoFuncion) {

		String strLinea;
		BufferedReader reader = new BufferedReader(new StringReader(
				textoFuncion));
		try {
			strLinea = reader.readLine();
			// Leer el archivo linea por linea

			while ((strLinea = reader.readLine()) != null) {

				parsearOperadores(strLinea);
				parsearOperandos(strLinea);

			}

			// Se arregla la cantidad del operador "="
			int aux = listaOperadores.get("=")
					- (listaOperadores.get("<=") + listaOperadores.get(">=")
							+ listaOperadores.get("!=") + listaOperadores
								.get("=="));
			listaOperadores.put("=", aux);

			// Se arregla la cantidad del operador "<"
			aux = listaOperadores.get("<") - listaOperadores.get("<=");
			listaOperadores.put("<", aux);

			// Se arregla la cantidad del operador ">"
			aux = listaOperadores.get(">") - listaOperadores.get(">=");
			listaOperadores.put(">", aux);

			// Se arregla la cantidad del operador "!"
			aux = listaOperadores.get("!") - listaOperadores.get("!=");
			listaOperadores.put("!", aux);

			// Se calculan los numero n1 y N1
			Iterator<String> iterador = listaOperadores.keySet().iterator();
			n1 = 0;
			N1 = 0;
			while (iterador.hasNext()) {

				String actualString = iterador.next();

				if (!listaOperadores.get(actualString).equals(0)) {
					n1++;
					N1 += listaOperadores.get(actualString);

				}

			}

			// Se calculan los numero n2 y N2
			iterador = listaOperandos.keySet().iterator();
			N2 = 0;
			n2 = 0;
			while (iterador.hasNext()) {

				String actualString = iterador.next();

				if (!listaOperandos.get(actualString).equals(0)) {
					n2++;
					N2 += listaOperandos.get(actualString);

				}

			}

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

}
